#include "precompiled.h"
#include "ShaderPool.h"
#include "Camera.h"
#include "Texture.h"
#include <unordered_map>	// http://www.codeguru.com/cpp/cpp/cpp_mfc/stl/article.php/c15303__2/A-TR1-Tutorial-Unordered-Containers.htm.

const char* DEFAULT_VS = "../shaders/diffuse.vp";
const char* DEFAULT_FS = "../shaders/diffuse.fp";

const char* NORMALDIFFUSE_VS = "../shaders/normal_diffuse.vp";
const char* NORMALDIFFUSE_FS = "../shaders/normal_diffuse.fp";

const char*  BILLBOARD_VS = "../shaders/billboard.vp";
const char*  BILLBOARD_GS = "../shaders/billboard.gp";
const char*  BILLBOARD_FS = "../shaders/billboard.fp";

namespace ShaderPool{
	

	GLhandleARB programObj;					// Shader objects.


	double shaderTime;
	// Uniform locations
	GLint		uniform_CameraPosition;
	GLint		uniform_ViewProjection;
	GLint		uniform_MVP;
	GLint		uniform_TextureColor;
	GLint		uniform_ScreenBuff;
	GLint		uniform_DoFSQ;
	GLfloat	uniform_time;
	GLfloat	uniform_flash;


	// View, Project, Transformations
	glm::mat4 mProjection;
	glm::mat4 mCam;
	glm::mat4 mMVP;


	const char *vertexShaderStrings[1];		// Spaces to store our shader strings.
	const char *fragmentShaderStrings[1];
	const char *geometryShaderStrings[1];
	GLint myVertexShaderCompiled, myFragmentShaderCompiled, myGeometryShaderCompiled, myShadersLinked;		// Compiled shader id's.
	char str[4096]; // For error messages from the GLSL compiler and linker

	shaderDesc current_shader;		// Our current shader.

	// Store our programs.
	std::tr1::unordered_map<std::string, shaderDesc> shader_desc_map;
	typedef std::tr1::unordered_map<std::string, shaderDesc> umap_id;

	
	/*
	 * createShaders() - create, load, compile and link and return the GLSL shader object.
	 */
	GLhandleARB createShader( shaderDesc* ptr_sd, std::string key, char const* vs_path, char const* fs_path ) {
		ptr_sd->key=key;
		ptr_sd->path_vs=vs_path;
		ptr_sd->path_gs="";
		ptr_sd->path_fs=fs_path;
		
		GLhandleARB prog;
	
		// Create the vertex shader.
		GLhandleARB vertex = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);

		  unsigned char *vertexShaderAssembly = readShaderFile( vs_path );
		vertexShaderStrings[0] = (char*)vertexShaderAssembly;
		glShaderSourceARB( vertex, 1, vertexShaderStrings, NULL );
		glCompileShaderARB( vertex);
		free((void *)vertexShaderAssembly);

		glGetObjectParameterivARB( vertex, GL_OBJECT_COMPILE_STATUS_ARB, 
								   &myVertexShaderCompiled );
		if(myVertexShaderCompiled  == GL_FALSE)
  		{
	  		glGetInfoLogARB(vertex, sizeof(str), NULL, str);
			std::string s("Vertex shader compile error [" + key + "] ");
			printError( s.c_str(), str);
		}
		ptr_sd->compVS = vertex;

		// Create the fragment shader.
		GLhandleARB fragment = glCreateShaderObjectARB( GL_FRAGMENT_SHADER_ARB );

		unsigned char *fragmentShaderAssembly = readShaderFile( fs_path );
		fragmentShaderStrings[0] = (char*)fragmentShaderAssembly;
		glShaderSourceARB( fragment, 1, fragmentShaderStrings, NULL );
		glCompileShaderARB( fragment );
		free((void *)fragmentShaderAssembly);

		glGetObjectParameterivARB( fragment, GL_OBJECT_COMPILE_STATUS_ARB, 
			&myFragmentShaderCompiled );
		if(myFragmentShaderCompiled == GL_FALSE)
		{
			glGetInfoLogARB( fragment, sizeof(str), NULL, str );
			std::string s("Fragment shader compile error [" + key + "] ");
			printError( s.c_str(), str);
		}
		ptr_sd->compFS = fragment;

		// Create a program object and attach the two compiled shaders.
		prog = glCreateProgramObjectARB();
		glAttachObjectARB( prog, vertex );
		glAttachObjectARB( prog, fragment );

		ptr_sd->compGS=0;


		/* Bind attribute index 0 (coordinates) to in_Position and attribute index 1 (color) to in_Color */
		/* Attribute locations must be setup before calling glLinkProgram. */
		glBindAttribLocationARB(prog, 0, "in_Position");
		glBindAttribLocationARB(prog, 1, "in_Normal"); 
		glBindAttribLocationARB(prog, 2, "in_TexCoord"); 
		glBindAttribLocationARB(prog, 3, "in_Color"); 

		// Link the program object and print out the info log.
		glLinkProgramARB( prog );
		glGetObjectParameterivARB( prog, GL_OBJECT_LINK_STATUS_ARB, &myShadersLinked );

		if( myShadersLinked == GL_FALSE )
		{
			glGetInfoLogARB( prog, sizeof(str), NULL, str );
			std::string s("Program compile error [" + key + "] ");
			printError( s.c_str(), str);

			return 0;
		}
		ptr_sd->program = prog;

		return prog;
	}

	GLhandleARB createShader(  shaderDesc* ptr_sd, std::string key, char const* vs_path, char const* gs_path, char const* fs_path ) {
		ptr_sd->key=key;
		ptr_sd->path_vs=vs_path;
		ptr_sd->path_gs=gs_path;
		ptr_sd->path_fs=fs_path;


		GLhandleARB prog;

		// Create the vertex shader.
		GLhandleARB vertex = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);

		unsigned char *vertexShaderAssembly = readShaderFile( vs_path );
		const char * vstrings[1];
		vstrings[0] = (char*)vertexShaderAssembly;
		glShaderSourceARB( vertex, 1, vstrings, NULL );
		glCompileShaderARB( vertex);
		free((void *)vertexShaderAssembly);

		glGetObjectParameterivARB( vertex, GL_OBJECT_COMPILE_STATUS_ARB, 
			&myVertexShaderCompiled );
		if(myVertexShaderCompiled  == GL_FALSE)
		{
			glGetInfoLogARB(vertex, sizeof(str), NULL, str);
			std::string s("Vertex shader compile error [" + key + "] ");
			printError( s.c_str(), str);
		}
		ptr_sd->compVS = vertex;

		// Create the fragment shader.
		GLhandleARB fragment = glCreateShaderObjectARB( GL_FRAGMENT_SHADER_ARB );

		unsigned char *fragmentShaderAssembly = readShaderFile( fs_path );
		const char * fstrings[1];
		fstrings[0] = (char*)fragmentShaderAssembly;
		glShaderSourceARB( fragment, 1, fstrings, NULL );
		glCompileShaderARB( fragment );
		free((void *)fragmentShaderAssembly);

		glGetObjectParameterivARB( fragment, GL_OBJECT_COMPILE_STATUS_ARB, 
			&myFragmentShaderCompiled );
		if(myFragmentShaderCompiled == GL_FALSE)
		{
			glGetInfoLogARB( fragment, sizeof(str), NULL, str );
			std::string s("Fragment shader compile error [" + key + "] ");
			printError( s.c_str(), str);
		}
		ptr_sd->compFS = fragment;

		// Create the geometry shader.
		GLhandleARB geometry = glCreateShaderObjectARB( GL_GEOMETRY_SHADER_ARB );

		unsigned char *geometryShaderAssembly = readShaderFile( gs_path );
		const char * gstrings[1];
		gstrings[0] = (char*)geometryShaderAssembly;
		glShaderSourceARB( geometry, 1, gstrings, NULL );
		glCompileShaderARB( geometry );
		free((void *)geometryShaderAssembly);

		glGetObjectParameterivARB( geometry, GL_OBJECT_COMPILE_STATUS_ARB, 
			&myGeometryShaderCompiled );
		if(myGeometryShaderCompiled == GL_FALSE)
		{
			glGetInfoLogARB( geometry, sizeof(str), NULL, str );
			std::string s("Geometry shader compile error [" + key + "] ");
			printError( s.c_str(), str);
		}
		ptr_sd->compGS = geometry;

		// Create a program object and attach the compiled shaders.
		prog = glCreateProgramObjectARB();
		glAttachObjectARB( prog, vertex );
		glAttachObjectARB( prog, fragment );
		glAttachObjectARB( prog, geometry );


		/* Bind attribute index 0 (coordinates) to in_Position and attribute index 1 (color) to in_Color */
		/* Attribute locations must be setup before calling glLinkProgram. */
		glBindAttribLocationARB(prog, 0, "in_Position");
		glBindAttribLocationARB(prog, 1, "in_Normal"); 
		glBindAttribLocationARB(prog, 2, "in_TexCoord"); 
		glBindAttribLocationARB(prog, 3, "in_Color"); 

		// Link the program object and print out the info log.
		glLinkProgramARB( prog );
		glGetObjectParameterivARB( prog, GL_OBJECT_LINK_STATUS_ARB, &myShadersLinked );

		if( myShadersLinked == GL_FALSE )
		{
			glGetInfoLogARB( prog, sizeof(str), NULL, str );
			std::string s("Program compile error [" + key + "] ");
			printError( s.c_str(), str);

			return 0;
		}
		ptr_sd->program = prog;

		return prog;
	}

	void print_pooledshader(char const* key, int program, GLhandleARB vs, GLhandleARB gs, GLhandleARB fs ){
		printf("Pooled shader[%s] program[%d] vs[%d] gs[%d] fs[%d]\n",key,program,vs,gs,fs);
	}
	void print_setshader(char const* key, int program, GLhandleARB vs, GLhandleARB gs, GLhandleARB fs ){
		printf("Set shader[%s] program[%d] vs[%d] gs[%d] fs[%d]\n",key,program,vs,gs,fs);
	}

	void PoolShaders(){
		GLuint prog = 0;
		std::string key("");

		{
			shaderDesc sd;
			key = Shaders::DEFAULT;
			prog = createShader( &sd, key, DEFAULT_VS, DEFAULT_FS );
			shader_desc_map.insert( std::make_pair(key, sd) );
			print_pooledshader( key.c_str(), prog,sd.compVS,sd.compGS,sd.compFS );
		}

		{
			shaderDesc sd;
			key = Shaders::NORMALDIFFUSE;
			prog = createShader( &sd, key, NORMALDIFFUSE_VS, NORMALDIFFUSE_FS );
			shader_desc_map.insert( std::make_pair(key, sd) );
			print_pooledshader( key.c_str(), prog,sd.compVS,sd.compGS,sd.compFS );
		}

		{
			shaderDesc sd;
			key = Shaders::BILLBOARD;
			prog = createShader( &sd, key, BILLBOARD_VS, BILLBOARD_GS, BILLBOARD_FS );
			shader_desc_map.insert( std::make_pair(key, sd) );
			print_pooledshader( key.c_str(), prog,sd.compVS,sd.compGS,sd.compFS );
		}

		return;
	}

	void SetUniforms( GLuint prog ){

		// Get uniform locations.
		uniform_CameraPosition = glGetUniformLocationARB(prog, "CameraPosition");
		uniform_ViewProjection = glGetUniformLocationARB(prog, "VP");
		uniform_MVP = glGetUniformLocationARB(prog, "MVP");
		uniform_TextureColor = glGetUniformLocationARB(prog, "colorMap");
		uniform_ScreenBuff = glGetUniformLocationARB(prog, "screenBuffer");
		uniform_DoFSQ = glGetUniformLocationARB(prog, "fsq");
		uniform_time = glGetUniformLocationARB(prog, "time");
		uniform_flash = glGetUniformLocationARB(prog, "flash");

	}
	bool isPooled( std::string key ){
		// Check if our key already exists.
		umap_id::iterator it = shader_desc_map.find( key );
		if( it == shader_desc_map.end() )
			return false;

		return true;
	}

	// Set the active shader.
	void SetShader( std::string key ){
		// Check if our key exists.
		if( !isPooled( key ) ){
			// If not, go to the default shader.
			current_shader = shader_desc_map.find( ShaderPool::Shaders::DEFAULT )->second;
			print_setshader( key.c_str(), current_shader.program,current_shader.compVS,current_shader.compGS,current_shader.compFS );
			programObj = current_shader.program;
			SetUniforms( programObj );
			return;//false;
		}

		// Find the position of the value and assign it.
		current_shader = shader_desc_map.find( key )->second;
		//print_setshader( key.c_str(), current_shader.program,current_shader.compVS,current_shader.compGS,current_shader.compFS );
		programObj = current_shader.program;
		SetUniforms( programObj );
	}

	void Initialize(){

		shaderTime = 0.0;

		// Uniform locations
		uniform_MVP = -1;
		uniform_TextureColor = -1;
		uniform_ScreenBuff = -1;
		uniform_DoFSQ = -1;
		uniform_time = -1;
		uniform_flash = -1;

		PoolShaders();
	}
	
	/*
	 * readShaderFile(filename) - read a shader source string from a file
	 */
	unsigned char* readShaderFile(const char *filename) {
		FILE *file = fopen(filename, "r");
		if(file == NULL)
		{
			printError("ERROR", "Cannot open shader file!");
  			  return 0;
		}
		int bytesinfile = filelength(fileno(file));
		unsigned char *buffer = (unsigned char*)malloc(bytesinfile+1);
		int bytesread = fread( buffer, 1, bytesinfile, file);
		buffer[bytesread] = 0; // Terminate the string with 0
		fclose(file);
    
		return buffer;
	}
	
	void OverwriteShaders( GLuint newProgram , std::string key ){
		// Find the position of the value and set it.
		shader_desc_map.find( key )->second.program = newProgram;
	}
	void reload_shaders(){

		// We need to create our shaders again.
		GLuint newProgram = createShader( &current_shader, current_shader.key, current_shader.path_vs.c_str(), current_shader.path_gs.c_str(), current_shader.path_fs.c_str() );

		// Update the map.
		OverwriteShaders( newProgram, current_shader.key );

		// We need to delete the old shaders.
		glDeleteProgram(programObj);

		// Use the new program.
		programObj = newProgram;
		SetUniforms( programObj );
		update_uniform_MVP_View();
	}

	void update_uniform_MVP_View(  ){

		// Possible uniforms.
		// http://www.opengl.org/sdk/docs/man/xhtml/glUniform.xml
		glUseProgramObjectARB( programObj );
		glUniformMatrix4fv(
			uniform_MVP, 1, GL_FALSE, &mMVP[0][0]); 
	}
	void update_uniform_MVP_View( glm::mat4 &modelViewProj  ){

		mMVP = modelViewProj;
		// Possible uniforms.
		// http://www.opengl.org/sdk/docs/man/xhtml/glUniform.xml
		glUseProgramObjectARB( programObj );
		glUniformMatrix4fv(
			uniform_MVP, 1, GL_FALSE, &mMVP[0][0]); 
	}
	// update the time we will use for our shaders.
	void update_uniform_time( GLdouble t0 ){
		GLdouble t = glfwGetTime() - t0;
		t *= 0.001;
		shaderTime = (shaderTime > 1.0)?0.0:shaderTime+t;

		//std::cout << "T: " << t << std::endl;
		//std::cout << "Shader time: " << shaderTime << std::endl;

		glUniform1fARB(uniform_time, shaderTime);
	}
	void update_uniform_flash( GLfloat amount ){
		glUniform1fARB( uniform_flash, amount);
	}
	void update_uniform_VP( glm::mat4 &ViewProj ){

		// Possible uniforms.
		// http://www.opengl.org/sdk/docs/man/xhtml/glUniform.xml
		glUseProgramObjectARB( programObj );
		glUniformMatrix4fv(
			uniform_ViewProjection, 1, GL_FALSE, &ViewProj[0][0]); 
	}
	void update_uniform_CameraPosition( glm::vec3 const& CameraPos ){

		glUniform3fARB(uniform_CameraPosition, CameraPos.x, CameraPos.y, CameraPos.z);
	}


	GLhandleARB ProgramObject(){
		return programObj;
	}

	void SetForFSQ(){
		glUniform1iARB(uniform_DoFSQ,1);
	}
	void BindTextureToScreenBuffer( Texture* t){
		t->Bind(1,uniform_ScreenBuff);
	}

}

const char* ShaderPool::Shaders::DEFAULT = "default";
const char* ShaderPool::Shaders::NORMALDIFFUSE = "normalDiffuse";
const char* ShaderPool::Shaders::BILLBOARD = "billboard";
